/* piexifjs

The MIT License (MIT)

Copyright (c) 2014, 2015 hMatoba(https://github.com/hMatoba)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

(function () {
  "use strict";
  var that = {};
  that.version = "1.0.4";

  that.remove = function (jpeg) {
      var b64 = false;
      if (jpeg.slice(0, 2) == "\xff\xd8") {
      } else if (jpeg.slice(0, 23) == "data:image/jpeg;base64," || jpeg.slice(0, 22) == "data:image/jpg;base64,") {
          jpeg = atob(jpeg.split(",")[1]);
          b64 = true;
      } else {
          throw new Error("Given data is not jpeg.");
      }
      
      var segments = splitIntoSegments(jpeg);
      var newSegments = segments.filter(function(seg){
        return  !(seg.slice(0, 2) == "\xff\xe1" &&
                 seg.slice(4, 10) == "Exif\x00\x00"); 
      });
      
      var new_data = newSegments.join("");
      if (b64) {
          new_data = "data:image/jpeg;base64," + btoa(new_data);
      }

      return new_data;
  };


  that.insert = function (exif, jpeg) {
      var b64 = false;
      if (exif.slice(0, 6) != "\x45\x78\x69\x66\x00\x00") {
          throw new Error("Given data is not exif.");
      }
      if (jpeg.slice(0, 2) == "\xff\xd8") {
      } else if (jpeg.slice(0, 23) == "data:image/jpeg;base64," || jpeg.slice(0, 22) == "data:image/jpg;base64,") {
          jpeg = atob(jpeg.split(",")[1]);
          b64 = true;
      } else {
          throw new Error("Given data is not jpeg.");
      }

      var exifStr = "\xff\xe1" + pack(">H", [exif.length + 2]) + exif;
      var segments = splitIntoSegments(jpeg);
      var new_data = mergeSegments(segments, exifStr);
      if (b64) {
          new_data = "data:image/jpeg;base64," + btoa(new_data);
      }

      return new_data;
  };


  that.load = function (data) {
      var input_data;
      if (typeof (data) == "string") {
          if (data.slice(0, 2) == "\xff\xd8") {
              input_data = data;
          } else if (data.slice(0, 23) == "data:image/jpeg;base64," || data.slice(0, 22) == "data:image/jpg;base64,") {
              input_data = atob(data.split(",")[1]);
          } else if (data.slice(0, 4) == "Exif") {
              input_data = data.slice(6);
          } else {
              throw new Error("'load' gots invalid file data.");
          }
      } else {
          throw new Error("'load' gots invalid type argument.");
      }

      var exifDict = {};
      var exif_dict = {
          "0th": {},
          "Exif": {},
          "GPS": {},
          "Interop": {},
          "1st": {},
          "thumbnail": null
      };
      var exifReader = new ExifReader(input_data);
      if (exifReader.tiftag === null) {
          return exif_dict;
      }

      if (exifReader.tiftag.slice(0, 2) == "\x49\x49") {
          exifReader.endian_mark = "<";
      } else {
          exifReader.endian_mark = ">";
      }

      var pointer = unpack(exifReader.endian_mark + "L",
          exifReader.tiftag.slice(4, 8))[0];
      exif_dict["0th"] = exifReader.get_ifd(pointer, "0th");

      var first_ifd_pointer = exif_dict["0th"]["first_ifd_pointer"];
      delete exif_dict["0th"]["first_ifd_pointer"];

      if (34665 in exif_dict["0th"]) {
          pointer = exif_dict["0th"][34665];
          exif_dict["Exif"] = exifReader.get_ifd(pointer, "Exif");
      }
      if (34853 in exif_dict["0th"]) {
          pointer = exif_dict["0th"][34853];
          exif_dict["GPS"] = exifReader.get_ifd(pointer, "GPS");
      }
      if (40965 in exif_dict["Exif"]) {
          pointer = exif_dict["Exif"][40965];
          exif_dict["Interop"] = exifReader.get_ifd(pointer, "Interop");
      }
      if (first_ifd_pointer != "\x00\x00\x00\x00") {
          pointer = unpack(exifReader.endian_mark + "L",
              first_ifd_pointer)[0];
          exif_dict["1st"] = exifReader.get_ifd(pointer, "1st");
          if ((513 in exif_dict["1st"]) && (514 in exif_dict["1st"])) {
              var end = exif_dict["1st"][513] + exif_dict["1st"][514];
              var thumb = exifReader.tiftag.slice(exif_dict["1st"][513], end);
              exif_dict["thumbnail"] = thumb;
          }
      }

      return exif_dict;
  };


  that.dump = function (exif_dict_original) {
      var TIFF_HEADER_LENGTH = 8;

      var exif_dict = copy(exif_dict_original);
      var header = "Exif\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08";
      var exif_is = false;
      var gps_is = false;
      var interop_is = false;
      var first_is = false;

      var zeroth_ifd,
          exif_ifd,
          interop_ifd,
          gps_ifd,
          first_ifd;
      
      if ("0th" in exif_dict) {
          zeroth_ifd = exif_dict["0th"];
      } else {
          zeroth_ifd = {};
      }
      
      if ((("Exif" in exif_dict) && (Object.keys(exif_dict["Exif"]).length)) ||
          (("Interop" in exif_dict) && (Object.keys(exif_dict["Interop"]).length))) {
          zeroth_ifd[34665] = 1;
          exif_is = true;
          exif_ifd = exif_dict["Exif"];
          if (("Interop" in exif_dict) && Object.keys(exif_dict["Interop"]).length) {
              exif_ifd[40965] = 1;
              interop_is = true;
              interop_ifd = exif_dict["Interop"];
          } else if (Object.keys(exif_ifd).indexOf(that.ExifIFD.InteroperabilityTag.toString()) > -1) {
              delete exif_ifd[40965];
          }
      } else if (Object.keys(zeroth_ifd).indexOf(that.ImageIFD.ExifTag.toString()) > -1) {
          delete zeroth_ifd[34665];
      }

      if (("GPS" in exif_dict) && (Object.keys(exif_dict["GPS"]).length)) {
          zeroth_ifd[that.ImageIFD.GPSTag] = 1;
          gps_is = true;
          gps_ifd = exif_dict["GPS"];
      } else if (Object.keys(zeroth_ifd).indexOf(that.ImageIFD.GPSTag.toString()) > -1) {
          delete zeroth_ifd[that.ImageIFD.GPSTag];
      }
      
      if (("1st" in exif_dict) &&
          ("thumbnail" in exif_dict) &&
          (exif_dict["thumbnail"] != null)) {
          first_is = true;
          exif_dict["1st"][513] = 1;
          exif_dict["1st"][514] = 1;
          first_ifd = exif_dict["1st"];
      }
      
      var zeroth_set = _dict_to_bytes(zeroth_ifd, "0th", 0);
      var zeroth_length = (zeroth_set[0].length + exif_is * 12 + gps_is * 12 + 4 +
          zeroth_set[1].length);

      var exif_set,
          exif_bytes = "",
          exif_length = 0,
          gps_set,
          gps_bytes = "",
          gps_length = 0,
          interop_set,
          interop_bytes = "",
          interop_length = 0,
          first_set,
          first_bytes = "",
          thumbnail;
      if (exif_is) {
          exif_set = _dict_to_bytes(exif_ifd, "Exif", zeroth_length);
          exif_length = exif_set[0].length + interop_is * 12 + exif_set[1].length;
      }
      if (gps_is) {
          gps_set = _dict_to_bytes(gps_ifd, "GPS", zeroth_length + exif_length);
          gps_bytes = gps_set.join("");
          gps_length = gps_bytes.length;
      }
      if (interop_is) {
          var offset = zeroth_length + exif_length + gps_length;
          interop_set = _dict_to_bytes(interop_ifd, "Interop", offset);
          interop_bytes = interop_set.join("");
          interop_length = interop_bytes.length;
      }
      if (first_is) {
          var offset = zeroth_length + exif_length + gps_length + interop_length;
          first_set = _dict_to_bytes(first_ifd, "1st", offset);
          thumbnail = _get_thumbnail(exif_dict["thumbnail"]);
          if (thumbnail.length > 64000) {
              throw new Error("Given thumbnail is too large. max 64kB");
          }
      }

      var exif_pointer = "",
          gps_pointer = "",
          interop_pointer = "",
          first_ifd_pointer = "\x00\x00\x00\x00";
      if (exif_is) {
          var pointer_value = TIFF_HEADER_LENGTH + zeroth_length;
          var pointer_str = pack(">L", [pointer_value]);
          var key = 34665;
          var key_str = pack(">H", [key]);
          var type_str = pack(">H", [TYPES["Long"]]);
          var length_str = pack(">L", [1]);
          exif_pointer = key_str + type_str + length_str + pointer_str;
      }
      if (gps_is) {
          var pointer_value = TIFF_HEADER_LENGTH + zeroth_length + exif_length;
          var pointer_str = pack(">L", [pointer_value]);
          var key = 34853;
          var key_str = pack(">H", [key]);
          var type_str = pack(">H", [TYPES["Long"]]);
          var length_str = pack(">L", [1]);
          gps_pointer = key_str + type_str + length_str + pointer_str;
      }
      if (interop_is) {
          var pointer_value = (TIFF_HEADER_LENGTH +
              zeroth_length + exif_length + gps_length);
          var pointer_str = pack(">L", [pointer_value]);
          var key = 40965;
          var key_str = pack(">H", [key]);
          var type_str = pack(">H", [TYPES["Long"]]);
          var length_str = pack(">L", [1]);
          interop_pointer = key_str + type_str + length_str + pointer_str;
      }
      if (first_is) {
          var pointer_value = (TIFF_HEADER_LENGTH + zeroth_length +
              exif_length + gps_length + interop_length);
          first_ifd_pointer = pack(">L", [pointer_value]);
          var thumbnail_pointer = (pointer_value + first_set[0].length + 24 +
              4 + first_set[1].length);
          var thumbnail_p_bytes = ("\x02\x01\x00\x04\x00\x00\x00\x01" +
              pack(">L", [thumbnail_pointer]));
          var thumbnail_length_bytes = ("\x02\x02\x00\x04\x00\x00\x00\x01" +
              pack(">L", [thumbnail.length]));
          first_bytes = (first_set[0] + thumbnail_p_bytes +
              thumbnail_length_bytes + "\x00\x00\x00\x00" +
              first_set[1] + thumbnail);
      }

      var zeroth_bytes = (zeroth_set[0] + exif_pointer + gps_pointer +
          first_ifd_pointer + zeroth_set[1]);
      if (exif_is) {
          exif_bytes = exif_set[0] + interop_pointer + exif_set[1];
      }

      return (header + zeroth_bytes + exif_bytes + gps_bytes +
          interop_bytes + first_bytes);
  };


  function copy(obj) {
      return JSON.parse(JSON.stringify(obj));
  }


  function _get_thumbnail(jpeg) {
      var segments = splitIntoSegments(jpeg);
      while (("\xff\xe0" <= segments[1].slice(0, 2)) && (segments[1].slice(0, 2) <= "\xff\xef")) {
          segments = [segments[0]].concat(segments.slice(2));
      }
      return segments.join("");
  }


  function _pack_byte(array) {
      return pack(">" + nStr("B", array.length), array);
  }


  function _pack_short(array) {
      return pack(">" + nStr("H", array.length), array);
  }


  function _pack_long(array) {
      return pack(">" + nStr("L", array.length), array);
  }


  function _value_to_bytes(raw_value, value_type, offset) {
      var four_bytes_over = "";
      var value_str = "";
      var length,
          new_value,
          num,
          den;

      if (value_type == "Byte") {
          length = raw_value.length;
          if (length <= 4) {
              value_str = (_pack_byte(raw_value) +
                  nStr("\x00", 4 - length));
          } else {
              value_str = pack(">L", [offset]);
              four_bytes_over = _pack_byte(raw_value);
          }
      } else if (value_type == "Short") {
          length = raw_value.length;
          if (length <= 2) {
              value_str = (_pack_short(raw_value) +
                  nStr("\x00\x00", 2 - length));
          } else {
              value_str = pack(">L", [offset]);
              four_bytes_over = _pack_short(raw_value);
          }
      } else if (value_type == "Long") {
          length = raw_value.length;
          if (length <= 1) {
              value_str = _pack_long(raw_value);
          } else {
              value_str = pack(">L", [offset]);
              four_bytes_over = _pack_long(raw_value);
          }
      } else if (value_type == "Ascii") {
          new_value = raw_value + "\x00";
          length = new_value.length;
          if (length > 4) {
              value_str = pack(">L", [offset]);
              four_bytes_over = new_value;
          } else {
              value_str = new_value + nStr("\x00", 4 - length);
          }
      } else if (value_type == "Rational") {
          if (typeof (raw_value[0]) == "number") {
              length = 1;
              num = raw_value[0];
              den = raw_value[1];
              new_value = pack(">L", [num]) + pack(">L", [den]);
          } else {
              length = raw_value.length;
              new_value = "";
              for (var n = 0; n < length; n++) {
                  num = raw_value[n][0];
                  den = raw_value[n][1];
                  new_value += (pack(">L", [num]) +
                      pack(">L", [den]));
              }
          }
          value_str = pack(">L", [offset]);
          four_bytes_over = new_value;
      } else if (value_type == "SRational") {
          if (typeof (raw_value[0]) == "number") {
              length = 1;
              num = raw_value[0];
              den = raw_value[1];
              new_value = pack(">l", [num]) + pack(">l", [den]);
          } else {
              length = raw_value.length;
              new_value = "";
              for (var n = 0; n < length; n++) {
                  num = raw_value[n][0];
                  den = raw_value[n][1];
                  new_value += (pack(">l", [num]) +
                      pack(">l", [den]));
              }
          }
          value_str = pack(">L", [offset]);
          four_bytes_over = new_value;
      } else if (value_type == "Undefined") {
          length = raw_value.length;
          if (length > 4) {
              value_str = pack(">L", [offset]);
              four_bytes_over = raw_value;
          } else {
              value_str = raw_value + nStr("\x00", 4 - length);
          }
      }

      var length_str = pack(">L", [length]);

      return [length_str, value_str, four_bytes_over];
  }

  function _dict_to_bytes(ifd_dict, ifd, ifd_offset) {
      var TIFF_HEADER_LENGTH = 8;
      var tag_count = Object.keys(ifd_dict).length;
      var entry_header = pack(">H", [tag_count]);
      var entries_length;
      if (["0th", "1st"].indexOf(ifd) > -1) {
          entries_length = 2 + tag_count * 12 + 4;
      } else {
          entries_length = 2 + tag_count * 12;
      }
      var entries = "";
      var values = "";
      var key;

      for (var key in ifd_dict) {
          if (typeof (key) == "string") {
              key = parseInt(key);
          }
          if ((ifd == "0th") && ([34665, 34853].indexOf(key) > -1)) {
              continue;
          } else if ((ifd == "Exif") && (key == 40965)) {
              continue;
          } else if ((ifd == "1st") && ([513, 514].indexOf(key) > -1)) {
              continue;
          }

          var raw_value = ifd_dict[key];
          var key_str = pack(">H", [key]);
          var value_type = TAGS[ifd][key]["type"];
          var type_str = pack(">H", [TYPES[value_type]]);

          if (typeof (raw_value) == "number") {
              raw_value = [raw_value];
          }
          var offset = TIFF_HEADER_LENGTH + entries_length + ifd_offset + values.length;
          var b = _value_to_bytes(raw_value, value_type, offset);
          var length_str = b[0];
          var value_str = b[1];
          var four_bytes_over = b[2];

          entries += key_str + type_str + length_str + value_str;
          values += four_bytes_over;
      }

      return [entry_header + entries, values];
  }



  function ExifReader(data) {
      var segments,
          app1;
      if (data.slice(0, 2) == "\xff\xd8") { // JPEG
          segments = splitIntoSegments(data);
          app1 = getExifSeg(segments);
          if (app1) {
              this.tiftag = app1.slice(10);
          } else {
              this.tiftag = null;
          }
      } else if (["\x49\x49", "\x4d\x4d"].indexOf(data.slice(0, 2)) > -1) { // TIFF
          this.tiftag = data;
      } else if (data.slice(0, 4) == "Exif") { // Exif
          this.tiftag = data.slice(6);
      } else {
          throw new Error("Given file is neither JPEG nor TIFF.");
      }
  }

  ExifReader.prototype = {
      get_ifd: function (pointer, ifd_name) {
          var ifd_dict = {};
          var tag_count = unpack(this.endian_mark + "H",
              this.tiftag.slice(pointer, pointer + 2))[0];
          var offset = pointer + 2;
          var t;
          if (["0th", "1st"].indexOf(ifd_name) > -1) {
              t = "Image";
          } else {
              t = ifd_name;
          }

          for (var x = 0; x < tag_count; x++) {
              pointer = offset + 12 * x;
              var tag = unpack(this.endian_mark + "H",
                  this.tiftag.slice(pointer, pointer + 2))[0];
              var value_type = unpack(this.endian_mark + "H",
                  this.tiftag.slice(pointer + 2, pointer + 4))[0];
              var value_num = unpack(this.endian_mark + "L",
                  this.tiftag.slice(pointer + 4, pointer + 8))[0];
              var value = this.tiftag.slice(pointer + 8, pointer + 12);

              var v_set = [value_type, value_num, value];
              if (tag in TAGS[t]) {
                  ifd_dict[tag] = this.convert_value(v_set);
              }
          }

          if (ifd_name == "0th") {
              pointer = offset + 12 * tag_count;
              ifd_dict["first_ifd_pointer"] = this.tiftag.slice(pointer, pointer + 4);
          }

          return ifd_dict;
      },

      convert_value: function (val) {
          var data = null;
          var t = val[0];
          var length = val[1];
          var value = val[2];
          var pointer;

          if (t == 1) { // BYTE
              if (length > 4) {
                  pointer = unpack(this.endian_mark + "L", value)[0];
                  data = unpack(this.endian_mark + nStr("B", length),
                      this.tiftag.slice(pointer, pointer + length));
              } else {
                  data = unpack(this.endian_mark + nStr("B", length), value.slice(0, length));
              }
          } else if (t == 2) { // ASCII
              if (length > 4) {
                  pointer = unpack(this.endian_mark + "L", value)[0];
                  data = this.tiftag.slice(pointer, pointer + length - 1);
              } else {
                  data = value.slice(0, length - 1);
              }
          } else if (t == 3) { // SHORT
              if (length > 2) {
                  pointer = unpack(this.endian_mark + "L", value)[0];
                  data = unpack(this.endian_mark + nStr("H", length),
                      this.tiftag.slice(pointer, pointer + length * 2));
              } else {
                  data = unpack(this.endian_mark + nStr("H", length),
                      value.slice(0, length * 2));
              }
          } else if (t == 4) { // LONG
              if (length > 1) {
                  pointer = unpack(this.endian_mark + "L", value)[0];
                  data = unpack(this.endian_mark + nStr("L", length),
                      this.tiftag.slice(pointer, pointer + length * 4));
              } else {
                  data = unpack(this.endian_mark + nStr("L", length),
                      value);
              }
          } else if (t == 5) { // RATIONAL
              pointer = unpack(this.endian_mark + "L", value)[0];
              if (length > 1) {
                  data = [];
                  for (var x = 0; x < length; x++) {
                      data.push([unpack(this.endian_mark + "L",
                              this.tiftag.slice(pointer + x * 8, pointer + 4 + x * 8))[0],
                                 unpack(this.endian_mark + "L",
                              this.tiftag.slice(pointer + 4 + x * 8, pointer + 8 + x * 8))[0]
                                 ]);
                  }
              } else {
                  data = [unpack(this.endian_mark + "L",
                          this.tiftag.slice(pointer, pointer + 4))[0],
                          unpack(this.endian_mark + "L",
                          this.tiftag.slice(pointer + 4, pointer + 8))[0]
                          ];
              }
          } else if (t == 7) { // UNDEFINED BYTES
              if (length > 4) {
                  pointer = unpack(this.endian_mark + "L", value)[0];
                  data = this.tiftag.slice(pointer, pointer + length);
              } else {
                  data = value.slice(0, length);
              }
          } else if (t == 9) { // SLONG
              if (length > 1) {
                  pointer = unpack(this.endian_mark + "L", value)[0];
                  data = unpack(this.endian_mark + nStr("l", length),
                      this.tiftag.slice(pointer, pointer + length * 4));
              } else {
                  data = unpack(this.endian_mark + nStr("l", length),
                      value);
              }
          } else if (t == 10) { // SRATIONAL
              pointer = unpack(this.endian_mark + "L", value)[0];
              if (length > 1) {
                  data = [];
                  for (var x = 0; x < length; x++) {
                      data.push([unpack(this.endian_mark + "l",
                              this.tiftag.slice(pointer + x * 8, pointer + 4 + x * 8))[0],
                                 unpack(this.endian_mark + "l",
                              this.tiftag.slice(pointer + 4 + x * 8, pointer + 8 + x * 8))[0]
                                ]);
                  }
              } else {
                  data = [unpack(this.endian_mark + "l",
                          this.tiftag.slice(pointer, pointer + 4))[0],
                          unpack(this.endian_mark + "l",
                          this.tiftag.slice(pointer + 4, pointer + 8))[0]
                         ];
              }
          } else {
              throw new Error("Exif might be wrong. Got incorrect value " +
                  "type to decode. type:" + t);
          }

          if ((data instanceof Array) && (data.length == 1)) {
              return data[0];
          } else {
              return data;
          }
      },
  };


  if (typeof window !== "undefined" && typeof window.btoa === "function") {
      var btoa = window.btoa;
  }
  if (typeof btoa === "undefined") {
      var btoa = function (input) {        var output = "";
          var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
          var i = 0;
          var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

          while (i < input.length) {

              chr1 = input.charCodeAt(i++);
              chr2 = input.charCodeAt(i++);
              chr3 = input.charCodeAt(i++);

              enc1 = chr1 >> 2;
              enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
              enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
              enc4 = chr3 & 63;

              if (isNaN(chr2)) {
                  enc3 = enc4 = 64;
              } else if (isNaN(chr3)) {
                  enc4 = 64;
              }

              output = output +
              keyStr.charAt(enc1) + keyStr.charAt(enc2) +
              keyStr.charAt(enc3) + keyStr.charAt(enc4);

          }

          return output;
      };
  }
  
  
  if (typeof window !== "undefined" && typeof window.atob === "function") {
      var atob = window.atob;
  }
  if (typeof atob === "undefined") {
      var atob = function (input) {
          var output = "";
          var chr1, chr2, chr3;
          var enc1, enc2, enc3, enc4;
          var i = 0;
          var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

          input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

          while (i < input.length) {

              enc1 = keyStr.indexOf(input.charAt(i++));
              enc2 = keyStr.indexOf(input.charAt(i++));
              enc3 = keyStr.indexOf(input.charAt(i++));
              enc4 = keyStr.indexOf(input.charAt(i++));

              chr1 = (enc1 << 2) | (enc2 >> 4);
              chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
              chr3 = ((enc3 & 3) << 6) | enc4;

              output = output + String.fromCharCode(chr1);

              if (enc3 != 64) {
                  output = output + String.fromCharCode(chr2);
              }
              if (enc4 != 64) {
                  output = output + String.fromCharCode(chr3);
              }

          }

          return output;
      };
  }


  function getImageSize(imageArray) {
      var segments = slice2Segments(imageArray);
      var seg,
          width,
          height,
          SOF = [192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207];

      for (var x = 0; x < segments.length; x++) {
          seg = segments[x];
          if (SOF.indexOf(seg[1]) >= 0) {
              height = seg[5] * 256 + seg[6];
              width = seg[7] * 256 + seg[8];
              break;
          }
      }
      return [width, height];
  }


  function pack(mark, array) {
      if (!(array instanceof Array)) {
          throw new Error("'pack' error. Got invalid type argument.");
      }
      if ((mark.length - 1) != array.length) {
          throw new Error("'pack' error. " + (mark.length - 1) + " marks, " + array.length + " elements.");
      }

      var littleEndian;
      if (mark[0] == "<") {
          littleEndian = true;
      } else if (mark[0] == ">") {
          littleEndian = false;
      } else {
          throw new Error("");
      }
      var packed = "";
      var p = 1;
      var val = null;
      var c = null;
      var valStr = null;

      while (c = mark[p]) {
          if (c.toLowerCase() == "b") {
              val = array[p - 1];
              if ((c == "b") && (val < 0)) {
                  val += 0x100;
              }
              if ((val > 0xff) || (val < 0)) {
                  throw new Error("'pack' error.");
              } else {
                  valStr = String.fromCharCode(val);
              }
          } else if (c == "H") {
              val = array[p - 1];
              if ((val > 0xffff) || (val < 0)) {
                  throw new Error("'pack' error.");
              } else {
                  valStr = String.fromCharCode(Math.floor((val % 0x10000) / 0x100)) +
                      String.fromCharCode(val % 0x100);
                  if (littleEndian) {
                      valStr = valStr.split("").reverse().join("");
                  }
              }
          } else if (c.toLowerCase() == "l") {
              val = array[p - 1];
              if ((c == "l") && (val < 0)) {
                  val += 0x100000000;
              }
              if ((val > 0xffffffff) || (val < 0)) {
                  throw new Error("'pack' error.");
              } else {
                  valStr = String.fromCharCode(Math.floor(val / 0x1000000)) +
                      String.fromCharCode(Math.floor((val % 0x1000000) / 0x10000)) +
                      String.fromCharCode(Math.floor((val % 0x10000) / 0x100)) +
                      String.fromCharCode(val % 0x100);
                  if (littleEndian) {
                      valStr = valStr.split("").reverse().join("");
                  }
              }
          } else {
              throw new Error("'pack' error.");
          }

          packed += valStr;
          p += 1;
      }

      return packed;
  }

  function unpack(mark, str) {
      if (typeof (str) != "string") {
          throw new Error("'unpack' error. Got invalid type argument.");
      }
      var l = 0;
      for (var markPointer = 1; markPointer < mark.length; markPointer++) {
          if (mark[markPointer].toLowerCase() == "b") {
              l += 1;
          } else if (mark[markPointer].toLowerCase() == "h") {
              l += 2;
          } else if (mark[markPointer].toLowerCase() == "l") {
              l += 4;
          } else {
              throw new Error("'unpack' error. Got invalid mark.");
          }
      }

      if (l != str.length) {
          throw new Error("'unpack' error. Mismatch between symbol and string length. " + l + ":" + str.length);
      }

      var littleEndian;
      if (mark[0] == "<") {
          littleEndian = true;
      } else if (mark[0] == ">") {
          littleEndian = false;
      } else {
          throw new Error("'unpack' error.");
      }
      var unpacked = [];
      var strPointer = 0;
      var p = 1;
      var val = null;
      var c = null;
      var length = null;
      var sliced = "";

      while (c = mark[p]) {
          if (c.toLowerCase() == "b") {
              length = 1;
              sliced = str.slice(strPointer, strPointer + length);
              val = sliced.charCodeAt(0);
              if ((c == "b") && (val >= 0x80)) {
                  val -= 0x100;
              }
          } else if (c == "H") {
              length = 2;
              sliced = str.slice(strPointer, strPointer + length);
              if (littleEndian) {
                  sliced = sliced.split("").reverse().join("");
              }
              val = sliced.charCodeAt(0) * 0x100 +
                  sliced.charCodeAt(1);
          } else if (c.toLowerCase() == "l") {
              length = 4;
              sliced = str.slice(strPointer, strPointer + length);
              if (littleEndian) {
                  sliced = sliced.split("").reverse().join("");
              }
              val = sliced.charCodeAt(0) * 0x1000000 +
                  sliced.charCodeAt(1) * 0x10000 +
                  sliced.charCodeAt(2) * 0x100 +
                  sliced.charCodeAt(3);
              if ((c == "l") && (val >= 0x80000000)) {
                  val -= 0x100000000;
              }
          } else {
              throw new Error("'unpack' error. " + c);
          }

          unpacked.push(val);
          strPointer += length;
          p += 1;
      }

      return unpacked;
  }

  function nStr(ch, num) {
      var str = "";
      for (var i = 0; i < num; i++) {
          str += ch;
      }
      return str;
  }

  function splitIntoSegments(data) {
      if (data.slice(0, 2) != "\xff\xd8") {
          throw new Error("Given data isn't JPEG.");
      }

      var head = 2;
      var segments = ["\xff\xd8"];
      while (true) {
          if (data.slice(head, head + 2) == "\xff\xda") {
              segments.push(data.slice(head));
              break;
          } else {
              var length = unpack(">H", data.slice(head + 2, head + 4))[0];
              var endPoint = head + length + 2;
              segments.push(data.slice(head, endPoint));
              head = endPoint;
          }

          if (head >= data.length) {
              throw new Error("Wrong JPEG data.");
          }
      }
      return segments;
  }


  function getExifSeg(segments) {
      var seg;
      for (var i = 0; i < segments.length; i++) {
          seg = segments[i];
          if (seg.slice(0, 2) == "\xff\xe1" &&
                 seg.slice(4, 10) == "Exif\x00\x00") {
              return seg;
          }
      }
      return null;
  }


  function mergeSegments(segments, exif) {
      var hasExifSegment = false;
      var additionalAPP1ExifSegments = [];

      segments.forEach(function(segment, i) {
          // Replace first occurence of APP1:Exif segment
          if (segment.slice(0, 2) == "\xff\xe1" &&
              segment.slice(4, 10) == "Exif\x00\x00"
          ) {
              if (!hasExifSegment) {
                  segments[i] = exif;
                  hasExifSegment = true;
              } else {
                  additionalAPP1ExifSegments.unshift(i);
              }
          }
      });

      // Remove additional occurences of APP1:Exif segment
      additionalAPP1ExifSegments.forEach(function(segmentIndex) {
          segments.splice(segmentIndex, 1);
      });

      if (!hasExifSegment && exif) {
          segments = [segments[0], exif].concat(segments.slice(1));
      }

      return segments.join("");
  }


  function toHex(str) {
      var hexStr = "";
      for (var i = 0; i < str.length; i++) {
          var h = str.charCodeAt(i);
          var hex = ((h < 10) ? "0" : "") + h.toString(16);
          hexStr += hex + " ";
      }
      return hexStr;
  }


  var TYPES = {
      "Byte": 1,
      "Ascii": 2,
      "Short": 3,
      "Long": 4,
      "Rational": 5,
      "Undefined": 7,
      "SLong": 9,
      "SRational": 10
  };


  var TAGS = {
      'Image': {
          11: {
              'name': 'ProcessingSoftware',
              'type': 'Ascii'
          },
          254: {
              'name': 'NewSubfileType',
              'type': 'Long'
          },
          255: {
              'name': 'SubfileType',
              'type': 'Short'
          },
          256: {
              'name': 'ImageWidth',
              'type': 'Long'
          },
          257: {
              'name': 'ImageLength',
              'type': 'Long'
          },
          258: {
              'name': 'BitsPerSample',
              'type': 'Short'
          },
          259: {
              'name': 'Compression',
              'type': 'Short'
          },
          262: {
              'name': 'PhotometricInterpretation',
              'type': 'Short'
          },
          263: {
              'name': 'Threshholding',
              'type': 'Short'
          },
          264: {
              'name': 'CellWidth',
              'type': 'Short'
          },
          265: {
              'name': 'CellLength',
              'type': 'Short'
          },
          266: {
              'name': 'FillOrder',
              'type': 'Short'
          },
          269: {
              'name': 'DocumentName',
              'type': 'Ascii'
          },
          270: {
              'name': 'ImageDescription',
              'type': 'Ascii'
          },
          271: {
              'name': 'Make',
              'type': 'Ascii'
          },
          272: {
              'name': 'Model',
              'type': 'Ascii'
          },
          273: {
              'name': 'StripOffsets',
              'type': 'Long'
          },
          274: {
              'name': 'Orientation',
              'type': 'Short'
          },
          277: {
              'name': 'SamplesPerPixel',
              'type': 'Short'
          },
          278: {
              'name': 'RowsPerStrip',
              'type': 'Long'
          },
          279: {
              'name': 'StripByteCounts',
              'type': 'Long'
          },
          282: {
              'name': 'XResolution',
              'type': 'Rational'
          },
          283: {
              'name': 'YResolution',
              'type': 'Rational'
          },
          284: {
              'name': 'PlanarConfiguration',
              'type': 'Short'
          },
          290: {
              'name': 'GrayResponseUnit',
              'type': 'Short'
          },
          291: {
              'name': 'GrayResponseCurve',
              'type': 'Short'
          },
          292: {
              'name': 'T4Options',
              'type': 'Long'
          },
          293: {
              'name': 'T6Options',
              'type': 'Long'
          },
          296: {
              'name': 'ResolutionUnit',
              'type': 'Short'
          },
          301: {
              'name': 'TransferFunction',
              'type': 'Short'
          },
          305: {
              'name': 'Software',
              'type': 'Ascii'
          },
          306: {
              'name': 'DateTime',
              'type': 'Ascii'
          },
          315: {
              'name': 'Artist',
              'type': 'Ascii'
          },
          316: {
              'name': 'HostComputer',
              'type': 'Ascii'
          },
          317: {
              'name': 'Predictor',
              'type': 'Short'
          },
          318: {
              'name': 'WhitePoint',
              'type': 'Rational'
          },
          319: {
              'name': 'PrimaryChromaticities',
              'type': 'Rational'
          },
          320: {
              'name': 'ColorMap',
              'type': 'Short'
          },
          321: {
              'name': 'HalftoneHints',
              'type': 'Short'
          },
          322: {
              'name': 'TileWidth',
              'type': 'Short'
          },
          323: {
              'name': 'TileLength',
              'type': 'Short'
          },
          324: {
              'name': 'TileOffsets',
              'type': 'Short'
          },
          325: {
              'name': 'TileByteCounts',
              'type': 'Short'
          },
          330: {
              'name': 'SubIFDs',
              'type': 'Long'
          },
          332: {
              'name': 'InkSet',
              'type': 'Short'
          },
          333: {
              'name': 'InkNames',
              'type': 'Ascii'
          },
          334: {
              'name': 'NumberOfInks',
              'type': 'Short'
          },
          336: {
              'name': 'DotRange',
              'type': 'Byte'
          },
          337: {
              'name': 'TargetPrinter',
              'type': 'Ascii'
          },
          338: {
              'name': 'ExtraSamples',
              'type': 'Short'
          },
          339: {
              'name': 'SampleFormat',
              'type': 'Short'
          },
          340: {
              'name': 'SMinSampleValue',
              'type': 'Short'
          },
          341: {
              'name': 'SMaxSampleValue',
              'type': 'Short'
          },
          342: {
              'name': 'TransferRange',
              'type': 'Short'
          },
          343: {
              'name': 'ClipPath',
              'type': 'Byte'
          },
          344: {
              'name': 'XClipPathUnits',
              'type': 'Long'
          },
          345: {
              'name': 'YClipPathUnits',
              'type': 'Long'
          },
          346: {
              'name': 'Indexed',
              'type': 'Short'
          },
          347: {
              'name': 'JPEGTables',
              'type': 'Undefined'
          },
          351: {
              'name': 'OPIProxy',
              'type': 'Short'
          },
          512: {
              'name': 'JPEGProc',
              'type': 'Long'
          },
          513: {
              'name': 'JPEGInterchangeFormat',
              'type': 'Long'
          },
          514: {
              'name': 'JPEGInterchangeFormatLength',
              'type': 'Long'
          },
          515: {
              'name': 'JPEGRestartInterval',
              'type': 'Short'
          },
          517: {
              'name': 'JPEGLosslessPredictors',
              'type': 'Short'
          },
          518: {
              'name': 'JPEGPointTransforms',
              'type': 'Short'
          },
          519: {
              'name': 'JPEGQTables',
              'type': 'Long'
          },
          520: {
              'name': 'JPEGDCTables',
              'type': 'Long'
          },
          521: {
              'name': 'JPEGACTables',
              'type': 'Long'
          },
          529: {
              'name': 'YCbCrCoefficients',
              'type': 'Rational'
          },
          530: {
              'name': 'YCbCrSubSampling',
              'type': 'Short'
          },
          531: {
              'name': 'YCbCrPositioning',
              'type': 'Short'
          },
          532: {
              'name': 'ReferenceBlackWhite',
              'type': 'Rational'
          },
          700: {
              'name': 'XMLPacket',
              'type': 'Byte'
          },
          18246: {
              'name': 'Rating',
              'type': 'Short'
          },
          18249: {
              'name': 'RatingPercent',
              'type': 'Short'
          },
          32781: {
              'name': 'ImageID',
              'type': 'Ascii'
          },
          33421: {
              'name': 'CFARepeatPatternDim',
              'type': 'Short'
          },
          33422: {
              'name': 'CFAPattern',
              'type': 'Byte'
          },
          33423: {
              'name': 'BatteryLevel',
              'type': 'Rational'
          },
          33432: {
              'name': 'Copyright',
              'type': 'Ascii'
          },
          33434: {
              'name': 'ExposureTime',
              'type': 'Rational'
          },
          34377: {
              'name': 'ImageResources',
              'type': 'Byte'
          },
          34665: {
              'name': 'ExifTag',
              'type': 'Long'
          },
          34675: {
              'name': 'InterColorProfile',
              'type': 'Undefined'
          },
          34853: {
              'name': 'GPSTag',
              'type': 'Long'
          },
          34857: {
              'name': 'Interlace',
              'type': 'Short'
          },
          34858: {
              'name': 'TimeZoneOffset',
              'type': 'Long'
          },
          34859: {
              'name': 'SelfTimerMode',
              'type': 'Short'
          },
          37387: {
              'name': 'FlashEnergy',
              'type': 'Rational'
          },
          37388: {
              'name': 'SpatialFrequencyResponse',
              'type': 'Undefined'
          },
          37389: {
              'name': 'Noise',
              'type': 'Undefined'
          },
          37390: {
              'name': 'FocalPlaneXResolution',
              'type': 'Rational'
          },
          37391: {
              'name': 'FocalPlaneYResolution',
              'type': 'Rational'
          },
          37392: {
              'name': 'FocalPlaneResolutionUnit',
              'type': 'Short'
          },
          37393: {
              'name': 'ImageNumber',
              'type': 'Long'
          },
          37394: {
              'name': 'SecurityClassification',
              'type': 'Ascii'
          },
          37395: {
              'name': 'ImageHistory',
              'type': 'Ascii'
          },
          37397: {
              'name': 'ExposureIndex',
              'type': 'Rational'
          },
          37398: {
              'name': 'TIFFEPStandardID',
              'type': 'Byte'
          },
          37399: {
              'name': 'SensingMethod',
              'type': 'Short'
          },
          40091: {
              'name': 'XPTitle',
              'type': 'Byte'
          },
          40092: {
              'name': 'XPComment',
              'type': 'Byte'
          },
          40093: {
              'name': 'XPAuthor',
              'type': 'Byte'
          },
          40094: {
              'name': 'XPKeywords',
              'type': 'Byte'
          },
          40095: {
              'name': 'XPSubject',
              'type': 'Byte'
          },
          50341: {
              'name': 'PrintImageMatching',
              'type': 'Undefined'
          },
          50706: {
              'name': 'DNGVersion',
              'type': 'Byte'
          },
          50707: {
              'name': 'DNGBackwardVersion',
              'type': 'Byte'
          },
          50708: {
              'name': 'UniqueCameraModel',
              'type': 'Ascii'
          },
          50709: {
              'name': 'LocalizedCameraModel',
              'type': 'Byte'
          },
          50710: {
              'name': 'CFAPlaneColor',
              'type': 'Byte'
          },
          50711: {
              'name': 'CFALayout',
              'type': 'Short'
          },
          50712: {
              'name': 'LinearizationTable',
              'type': 'Short'
          },
          50713: {
              'name': 'BlackLevelRepeatDim',
              'type': 'Short'
          },
          50714: {
              'name': 'BlackLevel',
              'type': 'Rational'
          },
          50715: {
              'name': 'BlackLevelDeltaH',
              'type': 'SRational'
          },
          50716: {
              'name': 'BlackLevelDeltaV',
              'type': 'SRational'
          },
          50717: {
              'name': 'WhiteLevel',
              'type': 'Short'
          },
          50718: {
              'name': 'DefaultScale',
              'type': 'Rational'
          },
          50719: {
              'name': 'DefaultCropOrigin',
              'type': 'Short'
          },
          50720: {
              'name': 'DefaultCropSize',
              'type': 'Short'
          },
          50721: {
              'name': 'ColorMatrix1',
              'type': 'SRational'
          },
          50722: {
              'name': 'ColorMatrix2',
              'type': 'SRational'
          },
          50723: {
              'name': 'CameraCalibration1',
              'type': 'SRational'
          },
          50724: {
              'name': 'CameraCalibration2',
              'type': 'SRational'
          },
          50725: {
              'name': 'ReductionMatrix1',
              'type': 'SRational'
          },
          50726: {
              'name': 'ReductionMatrix2',
              'type': 'SRational'
          },
          50727: {
              'name': 'AnalogBalance',
              'type': 'Rational'
          },
          50728: {
              'name': 'AsShotNeutral',
              'type': 'Short'
          },
          50729: {
              'name': 'AsShotWhiteXY',
              'type': 'Rational'
          },
          50730: {
              'name': 'BaselineExposure',
              'type': 'SRational'
          },
          50731: {
              'name': 'BaselineNoise',
              'type': 'Rational'
          },
          50732: {
              'name': 'BaselineSharpness',
              'type': 'Rational'
          },
          50733: {
              'name': 'BayerGreenSplit',
              'type': 'Long'
          },
          50734: {
              'name': 'LinearResponseLimit',
              'type': 'Rational'
          },
          50735: {
              'name': 'CameraSerialNumber',
              'type': 'Ascii'
          },
          50736: {
              'name': 'LensInfo',
              'type': 'Rational'
          },
          50737: {
              'name': 'ChromaBlurRadius',
              'type': 'Rational'
          },
          50738: {
              'name': 'AntiAliasStrength',
              'type': 'Rational'
          },
          50739: {
              'name': 'ShadowScale',
              'type': 'SRational'
          },
          50740: {
              'name': 'DNGPrivateData',
              'type': 'Byte'
          },
          50741: {
              'name': 'MakerNoteSafety',
              'type': 'Short'
          },
          50778: {
              'name': 'CalibrationIlluminant1',
              'type': 'Short'
          },
          50779: {
              'name': 'CalibrationIlluminant2',
              'type': 'Short'
          },
          50780: {
              'name': 'BestQualityScale',
              'type': 'Rational'
          },
          50781: {
              'name': 'RawDataUniqueID',
              'type': 'Byte'
          },
          50827: {
              'name': 'OriginalRawFileName',
              'type': 'Byte'
          },
          50828: {
              'name': 'OriginalRawFileData',
              'type': 'Undefined'
          },
          50829: {
              'name': 'ActiveArea',
              'type': 'Short'
          },
          50830: {
              'name': 'MaskedAreas',
              'type': 'Short'
          },
          50831: {
              'name': 'AsShotICCProfile',
              'type': 'Undefined'
          },
          50832: {
              'name': 'AsShotPreProfileMatrix',
              'type': 'SRational'
          },
          50833: {
              'name': 'CurrentICCProfile',
              'type': 'Undefined'
          },
          50834: {
              'name': 'CurrentPreProfileMatrix',
              'type': 'SRational'
          },
          50879: {
              'name': 'ColorimetricReference',
              'type': 'Short'
          },
          50931: {
              'name': 'CameraCalibrationSignature',
              'type': 'Byte'
          },
          50932: {
              'name': 'ProfileCalibrationSignature',
              'type': 'Byte'
          },
          50934: {
              'name': 'AsShotProfileName',
              'type': 'Byte'
          },
          50935: {
              'name': 'NoiseReductionApplied',
              'type': 'Rational'
          },
          50936: {
              'name': 'ProfileName',
              'type': 'Byte'
          },
          50937: {
              'name': 'ProfileHueSatMapDims',
              'type': 'Long'
          },
          50938: {
              'name': 'ProfileHueSatMapData1',
              'type': 'Float'
          },
          50939: {
              'name': 'ProfileHueSatMapData2',
              'type': 'Float'
          },
          50940: {
              'name': 'ProfileToneCurve',
              'type': 'Float'
          },
          50941: {
              'name': 'ProfileEmbedPolicy',
              'type': 'Long'
          },
          50942: {
              'name': 'ProfileCopyright',
              'type': 'Byte'
          },
          50964: {
              'name': 'ForwardMatrix1',
              'type': 'SRational'
          },
          50965: {
              'name': 'ForwardMatrix2',
              'type': 'SRational'
          },
          50966: {
              'name': 'PreviewApplicationName',
              'type': 'Byte'
          },
          50967: {
              'name': 'PreviewApplicationVersion',
              'type': 'Byte'
          },
          50968: {
              'name': 'PreviewSettingsName',
              'type': 'Byte'
          },
          50969: {
              'name': 'PreviewSettingsDigest',
              'type': 'Byte'
          },
          50970: {
              'name': 'PreviewColorSpace',
              'type': 'Long'
          },
          50971: {
              'name': 'PreviewDateTime',
              'type': 'Ascii'
          },
          50972: {
              'name': 'RawImageDigest',
              'type': 'Undefined'
          },
          50973: {
              'name': 'OriginalRawFileDigest',
              'type': 'Undefined'
          },
          50974: {
              'name': 'SubTileBlockSize',
              'type': 'Long'
          },
          50975: {
              'name': 'RowInterleaveFactor',
              'type': 'Long'
          },
          50981: {
              'name': 'ProfileLookTableDims',
              'type': 'Long'
          },
          50982: {
              'name': 'ProfileLookTableData',
              'type': 'Float'
          },
          51008: {
              'name': 'OpcodeList1',
              'type': 'Undefined'
          },
          51009: {
              'name': 'OpcodeList2',
              'type': 'Undefined'
          },
          51022: {
              'name': 'OpcodeList3',
              'type': 'Undefined'
          }
      },
      'Exif': {
          33434: {
              'name': 'ExposureTime',
              'type': 'Rational'
          },
          33437: {
              'name': 'FNumber',
              'type': 'Rational'
          },
          34850: {
              'name': 'ExposureProgram',
              'type': 'Short'
          },
          34852: {
              'name': 'SpectralSensitivity',
              'type': 'Ascii'
          },
          34855: {
              'name': 'ISOSpeedRatings',
              'type': 'Short'
          },
          34856: {
              'name': 'OECF',
              'type': 'Undefined'
          },
          34864: {
              'name': 'SensitivityType',
              'type': 'Short'
          },
          34865: {
              'name': 'StandardOutputSensitivity',
              'type': 'Long'
          },
          34866: {
              'name': 'RecommendedExposureIndex',
              'type': 'Long'
          },
          34867: {
              'name': 'ISOSpeed',
              'type': 'Long'
          },
          34868: {
              'name': 'ISOSpeedLatitudeyyy',
              'type': 'Long'
          },
          34869: {
              'name': 'ISOSpeedLatitudezzz',
              'type': 'Long'
          },
          36864: {
              'name': 'ExifVersion',
              'type': 'Undefined'
          },
          36867: {
              'name': 'DateTimeOriginal',
              'type': 'Ascii'
          },
          36868: {
              'name': 'DateTimeDigitized',
              'type': 'Ascii'
          },
          37121: {
              'name': 'ComponentsConfiguration',
              'type': 'Undefined'
          },
          37122: {
              'name': 'CompressedBitsPerPixel',
              'type': 'Rational'
          },
          37377: {
              'name': 'ShutterSpeedValue',
              'type': 'SRational'
          },
          37378: {
              'name': 'ApertureValue',
              'type': 'Rational'
          },
          37379: {
              'name': 'BrightnessValue',
              'type': 'SRational'
          },
          37380: {
              'name': 'ExposureBiasValue',
              'type': 'SRational'
          },
          37381: {
              'name': 'MaxApertureValue',
              'type': 'Rational'
          },
          37382: {
              'name': 'SubjectDistance',
              'type': 'Rational'
          },
          37383: {
              'name': 'MeteringMode',
              'type': 'Short'
          },
          37384: {
              'name': 'LightSource',
              'type': 'Short'
          },
          37385: {
              'name': 'Flash',
              'type': 'Short'
          },
          37386: {
              'name': 'FocalLength',
              'type': 'Rational'
          },
          37396: {
              'name': 'SubjectArea',
              'type': 'Short'
          },
          37500: {
              'name': 'MakerNote',
              'type': 'Undefined'
          },
          37510: {
              'name': 'UserComment',
              'type': 'Ascii'
          },
          37520: {
              'name': 'SubSecTime',
              'type': 'Ascii'
          },
          37521: {
              'name': 'SubSecTimeOriginal',
              'type': 'Ascii'
          },
          37522: {
              'name': 'SubSecTimeDigitized',
              'type': 'Ascii'
          },
          40960: {
              'name': 'FlashpixVersion',
              'type': 'Undefined'
          },
          40961: {
              'name': 'ColorSpace',
              'type': 'Short'
          },
          40962: {
              'name': 'PixelXDimension',
              'type': 'Long'
          },
          40963: {
              'name': 'PixelYDimension',
              'type': 'Long'
          },
          40964: {
              'name': 'RelatedSoundFile',
              'type': 'Ascii'
          },
          40965: {
              'name': 'InteroperabilityTag',
              'type': 'Long'
          },
          41483: {
              'name': 'FlashEnergy',
              'type': 'Rational'
          },
          41484: {
              'name': 'SpatialFrequencyResponse',
              'type': 'Undefined'
          },
          41486: {
              'name': 'FocalPlaneXResolution',
              'type': 'Rational'
          },
          41487: {
              'name': 'FocalPlaneYResolution',
              'type': 'Rational'
          },
          41488: {
              'name': 'FocalPlaneResolutionUnit',
              'type': 'Short'
          },
          41492: {
              'name': 'SubjectLocation',
              'type': 'Short'
          },
          41493: {
              'name': 'ExposureIndex',
              'type': 'Rational'
          },
          41495: {
              'name': 'SensingMethod',
              'type': 'Short'
          },
          41728: {
              'name': 'FileSource',
              'type': 'Undefined'
          },
          41729: {
              'name': 'SceneType',
              'type': 'Undefined'
          },
          41730: {
              'name': 'CFAPattern',
              'type': 'Undefined'
          },
          41985: {
              'name': 'CustomRendered',
              'type': 'Short'
          },
          41986: {
              'name': 'ExposureMode',
              'type': 'Short'
          },
          41987: {
              'name': 'WhiteBalance',
              'type': 'Short'
          },
          41988: {
              'name': 'DigitalZoomRatio',
              'type': 'Rational'
          },
          41989: {
              'name': 'FocalLengthIn35mmFilm',
              'type': 'Short'
          },
          41990: {
              'name': 'SceneCaptureType',
              'type': 'Short'
          },
          41991: {
              'name': 'GainControl',
              'type': 'Short'
          },
          41992: {
              'name': 'Contrast',
              'type': 'Short'
          },
          41993: {
              'name': 'Saturation',
              'type': 'Short'
          },
          41994: {
              'name': 'Sharpness',
              'type': 'Short'
          },
          41995: {
              'name': 'DeviceSettingDescription',
              'type': 'Undefined'
          },
          41996: {
              'name': 'SubjectDistanceRange',
              'type': 'Short'
          },
          42016: {
              'name': 'ImageUniqueID',
              'type': 'Ascii'
          },
          42032: {
              'name': 'CameraOwnerName',
              'type': 'Ascii'
          },
          42033: {
              'name': 'BodySerialNumber',
              'type': 'Ascii'
          },
          42034: {
              'name': 'LensSpecification',
              'type': 'Rational'
          },
          42035: {
              'name': 'LensMake',
              'type': 'Ascii'
          },
          42036: {
              'name': 'LensModel',
              'type': 'Ascii'
          },
          42037: {
              'name': 'LensSerialNumber',
              'type': 'Ascii'
          },
          42240: {
              'name': 'Gamma',
              'type': 'Rational'
          }
      },
      'GPS': {
          0: {
              'name': 'GPSVersionID',
              'type': 'Byte'
          },
          1: {
              'name': 'GPSLatitudeRef',
              'type': 'Ascii'
          },
          2: {
              'name': 'GPSLatitude',
              'type': 'Rational'
          },
          3: {
              'name': 'GPSLongitudeRef',
              'type': 'Ascii'
          },
          4: {
              'name': 'GPSLongitude',
              'type': 'Rational'
          },
          5: {
              'name': 'GPSAltitudeRef',
              'type': 'Byte'
          },
          6: {
              'name': 'GPSAltitude',
              'type': 'Rational'
          },
          7: {
              'name': 'GPSTimeStamp',
              'type': 'Rational'
          },
          8: {
              'name': 'GPSSatellites',
              'type': 'Ascii'
          },
          9: {
              'name': 'GPSStatus',
              'type': 'Ascii'
          },
          10: {
              'name': 'GPSMeasureMode',
              'type': 'Ascii'
          },
          11: {
              'name': 'GPSDOP',
              'type': 'Rational'
          },
          12: {
              'name': 'GPSSpeedRef',
              'type': 'Ascii'
          },
          13: {
              'name': 'GPSSpeed',
              'type': 'Rational'
          },
          14: {
              'name': 'GPSTrackRef',
              'type': 'Ascii'
          },
          15: {
              'name': 'GPSTrack',
              'type': 'Rational'
          },
          16: {
              'name': 'GPSImgDirectionRef',
              'type': 'Ascii'
          },
          17: {
              'name': 'GPSImgDirection',
              'type': 'Rational'
          },
          18: {
              'name': 'GPSMapDatum',
              'type': 'Ascii'
          },
          19: {
              'name': 'GPSDestLatitudeRef',
              'type': 'Ascii'
          },
          20: {
              'name': 'GPSDestLatitude',
              'type': 'Rational'
          },
          21: {
              'name': 'GPSDestLongitudeRef',
              'type': 'Ascii'
          },
          22: {
              'name': 'GPSDestLongitude',
              'type': 'Rational'
          },
          23: {
              'name': 'GPSDestBearingRef',
              'type': 'Ascii'
          },
          24: {
              'name': 'GPSDestBearing',
              'type': 'Rational'
          },
          25: {
              'name': 'GPSDestDistanceRef',
              'type': 'Ascii'
          },
          26: {
              'name': 'GPSDestDistance',
              'type': 'Rational'
          },
          27: {
              'name': 'GPSProcessingMethod',
              'type': 'Undefined'
          },
          28: {
              'name': 'GPSAreaInformation',
              'type': 'Undefined'
          },
          29: {
              'name': 'GPSDateStamp',
              'type': 'Ascii'
          },
          30: {
              'name': 'GPSDifferential',
              'type': 'Short'
          },
          31: {
              'name': 'GPSHPositioningError',
              'type': 'Rational'
          }
      },
      'Interop': {
          1: {
              'name': 'InteroperabilityIndex',
              'type': 'Ascii'
          }
      },
  };
  TAGS["0th"] = TAGS["Image"];
  TAGS["1st"] = TAGS["Image"];
  that.TAGS = TAGS;

  
  that.ImageIFD = {
      ProcessingSoftware:11,
      NewSubfileType:254,
      SubfileType:255,
      ImageWidth:256,
      ImageLength:257,
      BitsPerSample:258,
      Compression:259,
      PhotometricInterpretation:262,
      Threshholding:263,
      CellWidth:264,
      CellLength:265,
      FillOrder:266,
      DocumentName:269,
      ImageDescription:270,
      Make:271,
      Model:272,
      StripOffsets:273,
      Orientation:274,
      SamplesPerPixel:277,
      RowsPerStrip:278,
      StripByteCounts:279,
      XResolution:282,
      YResolution:283,
      PlanarConfiguration:284,
      GrayResponseUnit:290,
      GrayResponseCurve:291,
      T4Options:292,
      T6Options:293,
      ResolutionUnit:296,
      TransferFunction:301,
      Software:305,
      DateTime:306,
      Artist:315,
      HostComputer:316,
      Predictor:317,
      WhitePoint:318,
      PrimaryChromaticities:319,
      ColorMap:320,
      HalftoneHints:321,
      TileWidth:322,
      TileLength:323,
      TileOffsets:324,
      TileByteCounts:325,
      SubIFDs:330,
      InkSet:332,
      InkNames:333,
      NumberOfInks:334,
      DotRange:336,
      TargetPrinter:337,
      ExtraSamples:338,
      SampleFormat:339,
      SMinSampleValue:340,
      SMaxSampleValue:341,
      TransferRange:342,
      ClipPath:343,
      XClipPathUnits:344,
      YClipPathUnits:345,
      Indexed:346,
      JPEGTables:347,
      OPIProxy:351,
      JPEGProc:512,
      JPEGInterchangeFormat:513,
      JPEGInterchangeFormatLength:514,
      JPEGRestartInterval:515,
      JPEGLosslessPredictors:517,
      JPEGPointTransforms:518,
      JPEGQTables:519,
      JPEGDCTables:520,
      JPEGACTables:521,
      YCbCrCoefficients:529,
      YCbCrSubSampling:530,
      YCbCrPositioning:531,
      ReferenceBlackWhite:532,
      XMLPacket:700,
      Rating:18246,
      RatingPercent:18249,
      ImageID:32781,
      CFARepeatPatternDim:33421,
      CFAPattern:33422,
      BatteryLevel:33423,
      Copyright:33432,
      ExposureTime:33434,
      ImageResources:34377,
      ExifTag:34665,
      InterColorProfile:34675,
      GPSTag:34853,
      Interlace:34857,
      TimeZoneOffset:34858,
      SelfTimerMode:34859,
      FlashEnergy:37387,
      SpatialFrequencyResponse:37388,
      Noise:37389,
      FocalPlaneXResolution:37390,
      FocalPlaneYResolution:37391,
      FocalPlaneResolutionUnit:37392,
      ImageNumber:37393,
      SecurityClassification:37394,
      ImageHistory:37395,
      ExposureIndex:37397,
      TIFFEPStandardID:37398,
      SensingMethod:37399,
      XPTitle:40091,
      XPComment:40092,
      XPAuthor:40093,
      XPKeywords:40094,
      XPSubject:40095,
      PrintImageMatching:50341,
      DNGVersion:50706,
      DNGBackwardVersion:50707,
      UniqueCameraModel:50708,
      LocalizedCameraModel:50709,
      CFAPlaneColor:50710,
      CFALayout:50711,
      LinearizationTable:50712,
      BlackLevelRepeatDim:50713,
      BlackLevel:50714,
      BlackLevelDeltaH:50715,
      BlackLevelDeltaV:50716,
      WhiteLevel:50717,
      DefaultScale:50718,
      DefaultCropOrigin:50719,
      DefaultCropSize:50720,
      ColorMatrix1:50721,
      ColorMatrix2:50722,
      CameraCalibration1:50723,
      CameraCalibration2:50724,
      ReductionMatrix1:50725,
      ReductionMatrix2:50726,
      AnalogBalance:50727,
      AsShotNeutral:50728,
      AsShotWhiteXY:50729,
      BaselineExposure:50730,
      BaselineNoise:50731,
      BaselineSharpness:50732,
      BayerGreenSplit:50733,
      LinearResponseLimit:50734,
      CameraSerialNumber:50735,
      LensInfo:50736,
      ChromaBlurRadius:50737,
      AntiAliasStrength:50738,
      ShadowScale:50739,
      DNGPrivateData:50740,
      MakerNoteSafety:50741,
      CalibrationIlluminant1:50778,
      CalibrationIlluminant2:50779,
      BestQualityScale:50780,
      RawDataUniqueID:50781,
      OriginalRawFileName:50827,
      OriginalRawFileData:50828,
      ActiveArea:50829,
      MaskedAreas:50830,
      AsShotICCProfile:50831,
      AsShotPreProfileMatrix:50832,
      CurrentICCProfile:50833,
      CurrentPreProfileMatrix:50834,
      ColorimetricReference:50879,
      CameraCalibrationSignature:50931,
      ProfileCalibrationSignature:50932,
      AsShotProfileName:50934,
      NoiseReductionApplied:50935,
      ProfileName:50936,
      ProfileHueSatMapDims:50937,
      ProfileHueSatMapData1:50938,
      ProfileHueSatMapData2:50939,
      ProfileToneCurve:50940,
      ProfileEmbedPolicy:50941,
      ProfileCopyright:50942,
      ForwardMatrix1:50964,
      ForwardMatrix2:50965,
      PreviewApplicationName:50966,
      PreviewApplicationVersion:50967,
      PreviewSettingsName:50968,
      PreviewSettingsDigest:50969,
      PreviewColorSpace:50970,
      PreviewDateTime:50971,
      RawImageDigest:50972,
      OriginalRawFileDigest:50973,
      SubTileBlockSize:50974,
      RowInterleaveFactor:50975,
      ProfileLookTableDims:50981,
      ProfileLookTableData:50982,
      OpcodeList1:51008,
      OpcodeList2:51009,
      OpcodeList3:51022,
      NoiseProfile:51041,
  };

  
  that.ExifIFD = {
      ExposureTime:33434,
      FNumber:33437,
      ExposureProgram:34850,
      SpectralSensitivity:34852,
      ISOSpeedRatings:34855,
      OECF:34856,
      SensitivityType:34864,
      StandardOutputSensitivity:34865,
      RecommendedExposureIndex:34866,
      ISOSpeed:34867,
      ISOSpeedLatitudeyyy:34868,
      ISOSpeedLatitudezzz:34869,
      ExifVersion:36864,
      DateTimeOriginal:36867,
      DateTimeDigitized:36868,
      ComponentsConfiguration:37121,
      CompressedBitsPerPixel:37122,
      ShutterSpeedValue:37377,
      ApertureValue:37378,
      BrightnessValue:37379,
      ExposureBiasValue:37380,
      MaxApertureValue:37381,
      SubjectDistance:37382,
      MeteringMode:37383,
      LightSource:37384,
      Flash:37385,
      FocalLength:37386,
      SubjectArea:37396,
      MakerNote:37500,
      UserComment:37510,
      SubSecTime:37520,
      SubSecTimeOriginal:37521,
      SubSecTimeDigitized:37522,
      FlashpixVersion:40960,
      ColorSpace:40961,
      PixelXDimension:40962,
      PixelYDimension:40963,
      RelatedSoundFile:40964,
      InteroperabilityTag:40965,
      FlashEnergy:41483,
      SpatialFrequencyResponse:41484,
      FocalPlaneXResolution:41486,
      FocalPlaneYResolution:41487,
      FocalPlaneResolutionUnit:41488,
      SubjectLocation:41492,
      ExposureIndex:41493,
      SensingMethod:41495,
      FileSource:41728,
      SceneType:41729,
      CFAPattern:41730,
      CustomRendered:41985,
      ExposureMode:41986,
      WhiteBalance:41987,
      DigitalZoomRatio:41988,
      FocalLengthIn35mmFilm:41989,
      SceneCaptureType:41990,
      GainControl:41991,
      Contrast:41992,
      Saturation:41993,
      Sharpness:41994,
      DeviceSettingDescription:41995,
      SubjectDistanceRange:41996,
      ImageUniqueID:42016,
      CameraOwnerName:42032,
      BodySerialNumber:42033,
      LensSpecification:42034,
      LensMake:42035,
      LensModel:42036,
      LensSerialNumber:42037,
      Gamma:42240,
  };


  that.GPSIFD = {
      GPSVersionID:0,
      GPSLatitudeRef:1,
      GPSLatitude:2,
      GPSLongitudeRef:3,
      GPSLongitude:4,
      GPSAltitudeRef:5,
      GPSAltitude:6,
      GPSTimeStamp:7,
      GPSSatellites:8,
      GPSStatus:9,
      GPSMeasureMode:10,
      GPSDOP:11,
      GPSSpeedRef:12,
      GPSSpeed:13,
      GPSTrackRef:14,
      GPSTrack:15,
      GPSImgDirectionRef:16,
      GPSImgDirection:17,
      GPSMapDatum:18,
      GPSDestLatitudeRef:19,
      GPSDestLatitude:20,
      GPSDestLongitudeRef:21,
      GPSDestLongitude:22,
      GPSDestBearingRef:23,
      GPSDestBearing:24,
      GPSDestDistanceRef:25,
      GPSDestDistance:26,
      GPSProcessingMethod:27,
      GPSAreaInformation:28,
      GPSDateStamp:29,
      GPSDifferential:30,
      GPSHPositioningError:31,
  };


  that.InteropIFD = {
      InteroperabilityIndex:1,
  };

  that.GPSHelper = {
      degToDmsRational:function (degFloat) {
          var degAbs = Math.abs(degFloat);
          var minFloat = degAbs % 1 * 60;
          var secFloat = minFloat % 1 * 60;
          var deg = Math.floor(degAbs);
          var min = Math.floor(minFloat);
          var sec = Math.round(secFloat * 100);

          return [[deg, 1], [min, 1], [sec, 100]];
      },

      dmsRationalToDeg:function (dmsArray, ref) {
          var sign = (ref === 'S' || ref === 'W') ? -1.0 : 1.0;
          var deg = dmsArray[0][0] / dmsArray[0][1] +
                    dmsArray[1][0] / dmsArray[1][1] / 60.0 +
                    dmsArray[2][0] / dmsArray[2][1] / 3600.0;

          return deg * sign;
      }
  };
  
  
  if (typeof exports !== 'undefined') {
      if (typeof module !== 'undefined' && module.exports) {
          exports = module.exports = that;
      }
      exports.piexif = that;
  } else {
      window.piexif = that;
  }

})();
